Skip to content

Flash attn backend seam - #16

Open
eric-tc-wong wants to merge 3 commits into
mainfrom
flash-attn-backend-seam
Open

Flash attn backend seam#16
eric-tc-wong wants to merge 3 commits into
mainfrom
flash-attn-backend-seam

Conversation

@eric-tc-wong

Copy link
Copy Markdown

This pull request refactors the backend integration for FlashAttention kernels, consolidating backend selection and simplifying the interface for calling FlashAttention operations. It replaces direct imports and keyword-argument construction with new wrapper functions (fa_forward, fa_backward, etc.), and standardizes the way window size and other parameters are passed. The changes also add optional dependency documentation for alternative FlashAttention backends.

Backend abstraction and refactoring:

  • Replaces direct imports of FlashAttention kernel functions with wrapper functions (fa_forward, fa_backward, fa_varlen_forward, fa_varlen_backward) from the new flash_attn_backend module in all main attention modules (ring_flash_attn.py, ring_flash_attn_varlen.py, llama_fwd_ring_bwd_flash_attn.py, llama3_flash_attn_varlen.py). This centralizes backend selection and allows for easier swapping or patching of the backend. [1] [2] [3] [4]

  • Removes manual construction of parameter dictionaries and output unpacking for FlashAttention calls, replacing them with direct positional and keyword argument calls to the new backend wrappers. This reduces boilerplate and makes the code easier to follow and less error-prone. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]

Parameter handling improvements:

  • Standardizes the handling of the window_size parameter by passing it as a single argument instead of separate left/right values, simplifying the call interface throughout the codebase. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]

Backend support checks:

  • Adds calls to check_variant_supported at the start of top-level forward and backward functions to ensure that the chosen backend supports the requested operation, improving robustness. [1] [2] [3]

Optional dependency documentation:

  • Updates pyproject.toml to add fa3 and fa4 as optional dependencies, documenting how to install alternative FlashAttention backends for different hardware or versions.

eric-tc-wong and others added 3 commits July 31, 2026 20:08
The flash_attn dependency was not abstracted: 7 modules imported it directly
and 20 call sites each re-implemented the same three compatibility branches
inline (the 8-vs-4 forward tuple unpack, the window_size arity split, and the
torch.ops.flash_attn version gate).

Introduce ring_flash_attn/flash_attn_backend.py, which normalises the four
primitives the ring variants actually need -- fa_forward / fa_backward and
their varlen flavours -- onto the FA2 keyword set, and route every call site
through it. Backend selection is RING_FLASH_ATTN_BACKEND, defaulting to fa2,
so existing behaviour is unchanged unless a caller opts in.

FA4 notes: its cute interface returns a 2-tuple, needs an explicit
return_lse=True (autograd.Function.forward runs with grad disabled, so the
requires_grad shortcut never fires), takes no dropout/ALiBi, maps the -1
window sentinel to None, and registers no torch.library ops -- so it is not
traceable under torch.compile. Its backward additionally requires sm90+.

The loader raises rather than falling back between backends: flash-attn-4
installs into the flash_attn namespace, so a broken flash-attn 2.x makes FA4
unimportable too, and a silent fallback would change numerics and hardware
requirements underneath the caller.

Variants outside FA4 scope are refused via check_variant_supported rather than
run with unverified numerics. ring_flash_attn_backward is in the validated set
because both llama hybrid entry points route into it.

Verified against FA2: CPU tests plus 11 GPU tests and 7 compile-leg runs at
nproc_per_node 1, all metrics bit-identical to a stashed baseline except
dq diff, which is nondeterministic under FA2's atomic dq accumulation
(shown via repeat runs on both sides, and by confirming the seam's only added
kwargs are parameters passed at their own declared defaults).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FA3 ships as a separate flash_attn_3 package with its own interface. Add it as a
third backend behind the existing seam, selected with RING_FLASH_ATTN_BACKEND=fa3.
FA2 remains the default and is unchanged.

FA3 is a much closer fit for ring attention than FA4:

  * LSE layout matches FA2 exactly -- (batch, nheads, seqlen) dense and
    (nheads, total_q) varlen -- so update_out_and_lse needs no transpose
  * dq/dk/dv are written in place (mutates_args), FA3's only mode, so there is
    no copy-back shim
  * the -1 window sentinel is native; mapping it to None as FA4 needs would
    fail FA3's torch.library schema, which types these as plain int
  * both ops are real torch.library custom ops with register_fake, so unlike
    FA4 they are traceable under torch.compile

Three ways the signatures differ from FA2, each of which fails quietly rather
than loudly if copied:

  * backward renames causal to is_causal
  * backward puts cu_seqlens_*/max_seqlen_* BEFORE dq/dk/dv, where FA2 puts
    dq/dk/dv straight after softmax_lse, so a positional call misbinds
  * dq/dk/dv are Optional and the C++ allocates throwaways when omitted,
    computing the gradient and discarding it with no error

The adapter therefore calls keyword-only and always passes dq/dk/dv. Window
arity and the causal kwarg name are resolved by introspecting the real
signature via get_default_args, whose _init_fn fallback already handles
CustomOpDef -- older FA3 releases (still present as vendored hopper/ trees in
several envs here) use plain functions, a window_size tuple, and causal.

Varlen is unified upstream: one op per direction, varlen selected by passing
cu_seqlens_q. The four seam entry points are kept, with the varlen pair as thin
wrappers.

is_fa3_available probes flash_attn_3._C rather than the interface module,
because a bare hopper/ source tree with no built extension otherwise looks
available and dies at the first kernel call.

_FA4_VALIDATED_VARIANTS becomes a per-backend dict; fa3 shares fa4's llama-path
scope. _set_window moves to module scope, now shared by fa2 and fa3.

FA3 cannot run on this machine (no flash_attn_3._C, and stock builds gencode
sm90a), so it ships unrun. test_fa3_arg_binding covers what is checkable
without a GPU: it loads the seam under fa3 against a checkout's real ops with
the extension stubbed, and binds the adapter's own params dicts against the
true signatures, asserting on BoundArguments identity rather than just that
bind succeeded.

FA2 verified unchanged: CPU tests plus 11 GPU tests and 7 compile-leg runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The build-flag guard was dead. It looked up FLASH_ATTENTION_DISABLE_*, the
spelling of the env vars that *set* the flags, but the generated config keys are
FLASHATTENTION_DISABLE_* with no underscore after FLASH (hopper/setup.py:91), so
every lookup missed and no build was ever rejected. Values are real booleans
(setup.py:51 computes them as `== "TRUE"`), so plain truthiness is right once the
keys are.

The guard also only looked for a top-level `flash_attn_config`, which is what
hopper/setup.py's py_modules installs -- but the layout that exposes
flash_attn_3.flash_attn_interface keeps its config alongside, so the guard was a
no-op on exactly the install this backend targets. Derive the config module from
whichever interface module resolved, then fall back to the top-level name, and
treat any unrecognised shape as "no opinion" rather than a failed import.

Tests cover all of it: the guard fires on DISABLE_VARLEN and DISABLE_BACKWARD,
stays quiet on a healthy build, and tolerates a list/str/None/unknown-keys config.

Also assert that every kwarg the adapter passes but does not explicitly set equals
its own declared default -- the property that makes get_default_args' pass-everything
approach a semantic no-op, previously verified for FA2 but not FA3.

test.sh's cpu_tests ran `python -m unittest test.<name>`, which resolved to the
CPython stdlib `test` package because there is no test/__init__.py here; all three
entries errored on import and the block never tested anything. Run them as bare
module names with test/ on the path, and add the fa3 test so it is not orphaned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant